0X00 前言

代码审计的时候经常会遇到种类繁杂的转义函数,最可怕的是他们长的都很像,还是拿出来总结一下吧。

0X01 addslashes() –>(PHP 4, PHP 5, PHP 7)

用法:

string addslashes ( string $str )

返回值:

返回字符串,该字符串为了数据库查询语句等的需要在某些字符前加上了反斜线。这些字符是单引号(’)、双引号(”)、反斜线(\)与 NUL(NULL 字符)。

一个使用 addslashes() 的例子是当你要往数据库中输入数据时。 例如,将名字 O’reilly 插入到数据库中,这就需要对其进行转义。 强烈建议使用 DBMS 指定的转义函数 (比如 MySQL 是 mysqli_real_escape_string(),PostgreSQL 是 pg_escape_string()),但是如果你使用的 DBMS 没有一个转义函数,并且使用 \ 来转义特殊字符,你可以使用这个函数。 仅仅是为了获取插入数据库的数据,额外的 \ 并不会插入。 当 PHP 指令 magic_quotes_sybase 被设置成 on 时,意味着插入 ‘ 时将使用 ‘ 进行转义。

PHP 5.4 之前 PHP 指令 magic_quotes_gpc 默认是 on, 实际上所有的 GET、POST 和 COOKIE 数据都用被 addslashes() 了。 不要对已经被 magic_quotes_gpc 转义过的字符串使用 addslashes(),因为这样会导致双层转义。 遇到这种情况时可以使用函数 get_magic_quotes_gpc() 进行检测。

代码示例:

<?php
$str = "Is your name O'reilly?";

// 输出: Is your name O\'reilly?
echo addslashes($str);
?>

0X02 stripslashes() –>(PHP 4, PHP 5, PHP 7)

用法:

string stripslashes ( string $str )

反引用一个引用字符串,如果 magic_quotes_sybase 项开启,反斜线将被去除,但是两个反斜线将会被替换成一个。

返回值:

返回一个去除转义反斜线后的字符串(\’ 转换为 ‘ 等等)。双反斜线(\)被转换为单个反斜线(\)。

代码示例:

<?php
function stripslashes_deep($value)
{
    $value = is_array($value) ?
                array_map('stripslashes_deep', $value) :
                stripslashes($value);

    return $value;
}

// 范例
$array = array("f\\'oo", "b\\'ar", array("fo\\'o", "b\\'ar"));
$array = stripslashes_deep($array);

// 输出
print_r($array);
?>

结果:

Array
(
    [0] => f'oo
    [1] => b'ar
    [2] => Array
        (
            [0] => fo'o
            [1] => b'ar
        )

)

0X03 addcslashes() –>(PHP 4, PHP 5, PHP 7)

用法:

string addcslashes ( string $str , string $charlist )

返回值:

返回字符串,该字符串在属于参数 charlist 列表中的字符前都加上了反斜线。

示例代码:

这段代码就是告诉我们要注意选取的字符的范围,大写字母和小写字母中间还有一些可见字符,另外起始字符的ascII 码要小于结束符的,否则达不到预期的效果,只能是转义这个几个列出来的

<?php
echo addcslashes('foo[ ]', 'A..z');
// 输出:\f\o\o\[ \]
// 所有大小写字母均被转义
// ... 但 [\]^_` 以及分隔符、换行符、回车符等也一并被转义了。
?>

注意: 当选择对字符 0,a,b,f,n,r,t 和 v 进行转义时需要小心,它们将被转换成 \0,\a,\b,\f,\n,\r,\t 和 \v。在 PHP 中,只有 \0(NULL),\r(回车符),\n(换行符)和
\t(制表符)是预定义的转义序列, 而在 C 语言中,上述的所有转换后的字符都是预定义的转义序列。

0X04 stripcslashes() –>(PHP 4, PHP 5, PHP 7)

用法:

string stripcslashes ( string $str )

返回值:

返回反转义后的字符串。可识别类似 C 语言的 \n,\r,… 八进制以及十六进制的描述。

示例代码:

stripcslashes('He\xallo') == 'He'."\n".'llo'
stripcslashes('H\xaello') == 'H'.chr(0xAE).'llo'

0X05 mysql_escape_string() –>(PHP 4 >= 4.0.3, PHP 5)

用法:

string mysql_escape_string ( string $unescaped_string )

mysql_escape_string() 并不转义 % 和 _。 本函数和 mysql_real_escape_string() 完全一样,除了 mysql_real_escape_string() 接受的是一个连接句柄并根据当前字符集转移字符串之外。mysql_escape_string() 并不接受连接参数,也不管当前字符集设定。

示例代码:

<?php
    $item = "Zak's Laptop";
    $escaped_item = mysql_escape_string($item);
    printf ("Escaped string: %s\n", $escaped_item);
?>

结果:

Escaped string: Zak\'s Laptop

0X06 mysql_real_escape_string() –>(PHP 4 >= 4.3.0, PHP 5)

用法:

string mysql_real_escape_string ( string $unescaped_string [, resource $link_identifier = NULL ] )

本函数将 unescaped_string 中的特殊字符转义,并计及连接的当前字符集,因此可以安全用于 mysql_query()。

mysql_real_escape_string() 调用mysql库的函数 mysql_real_escape_string, 在以下字符前添加反斜杠: 

\x00
\n 
\r
\
'
"
\x1a.

为了安全起见,在像MySQL传送查询前,必须调用这个函数(除了少数例外情况)。

注意: 本扩展自 PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除。应使用 MySQLi 或 PDO_MySQL 扩展来替换之。

0X07 PHP 魔术引号 –> (< PHP 5.4)

1.什么是魔术引号

当打开时,所有的 ‘(单引号),”(双引号),\(反斜线)和 NULL 字符都会被自动加上一个反斜线进行转义。这和 addslashes() 作用完全相同。

一共有三个魔术引号指令:

(1)magic_quotes_gpc 影响到 HTTP 请求数据(GET,POST 和 COOKIE)。不能在运行时改变。在 PHP 中默认值为 on。

代码示例:

<?php
// 如果启用了魔术引号

echo $_POST['lastname'];             // O\'reilly
echo addslashes($_POST['lastname']); // O\\\'reilly

// 适用各个 PHP 版本的用法
if (get_magic_quotes_gpc()) {
    $lastname = stripslashes($_POST['lastname']);
}
else {
    $lastname = $_POST['lastname'];
}

// 如果使用 MySQL
$lastname = mysql_real_escape_string($lastname);

echo $lastname; // O\'reilly
$sql = "INSERT INTO lastnames (lastname) VALUES ('$lastname')";
?>

(2)magic_quotes_runtime 如果打开的话,大部份从外部来源取得数据并返回的函数,包括从数据库和文本文件,所返回的数据都会被反斜线转义。该选项可在运行的时改变,在 PHP 中的默认值为 off。

代码示例:

<?php
// 创建临时文件指针
$fp = tmpfile();

// 写入一些数据
fwrite($fp, '\'PHP\' is a Recursive acronym');

// 没有 magic_quotes_runtime
rewind($fp);
set_magic_quotes_runtime(false);

echo 'Without magic_quotes_runtime: ' . fread($fp, 64), PHP_EOL;

// 有 magic_quotes_runtime
rewind($fp);
set_magic_quotes_runtime(true);

echo 'With magic_quotes_runtime: ' . fread($fp, 64), PHP_EOL;

// 清理
fclose($fp);
?>

magic_quotes_gpc与magic_quotes_runtime的区别

1.magic_quotes_runtime是对外部引入的数据库资料或者文件中的特殊字符进行转义,而magic_quotes_gpc是对post、get、cookie等数组传递过来的数据进行特殊字符转义。

2.他们都有相应的get函数,可以对php环境中是否设置了他们相应功能特性进行探测,如:get_magic_quotes_gpc,是对magic_quotes_gpc是否设置的探测,get_magic_quotes_runtime,是对magic_quotes_runtime是否设置的探测,而且都是如果设置了,get函数返回1,如果没有设置,get函数返回0。

3.不能在程序里面设置magic_quotes_gpc的值,原因是php中并没有set_magic_quotes_gpc这个函数,而magic_quotes_runtime有对应的能在代码中直接设置magic_quotes_runtime值的函数:set_magic_quotes_runtime,所以,magic_quotes_gpc的值,只能自己手动在php.ini文件里面设置了。

(3)magic_quotes_sybase
如果该选项在php.ini文件中是唯一开启的话,将只会转义%00为\0(即null字符)。此选项会完全覆盖magic_quotes_gpc。如果同时开启这两个选项的话,单引号将会被转义成两个单引号,%00会被转义为\0。而双引号、反斜线将不会进行转义

1.设置:magic_quotes_sybase = On & magic_quotes_gpc = Off

输入:

1'2"3\4%005

结果:

1'2"3\45

结论:

只将%00(即null字符)过滤了

2.设置:magic_quotes_sybase = On & magic_quotes_gpc = On

输入:

1'2”3\4%005

结果:

1''2"3\4\05

结论:

magic_quotes_sybase = On & magic_quotes_gpc = On时,magic_quotes_sybase将会使用单引号对单引号进行转义,%00(即null字符)也会被转义。

2.为什么存在魔术引号

没有理由再使用魔术引号,因为它不再是 PHP 支持的一部分。不过它帮助了新手在不知不觉中写出了更好(更安全)的代码。但是在处理代码的时候,最好是更改你的代码而不是依赖于魔术引号的开启。 为什么这个功能存在?是为了阻止SQL 注入。在今天,开发者能够更好得意识到了安全问题,并最终使用数据库转移机制或者 prepared语句来取代魔术引号功能。

3.为什么不用魔术引号

(1)可移植性

编程时认为其打开或并闭都会影响到移植性。可以用 get_magic_quotes_gpc() 来检查是否打开,并据此编程。

(2)性能

由于并不是每一段被转义的数据都要插入数据库的,如果所有进入 PHP 的数据都被转义的话,那么会对程序的执行效率产生一定的影响。在运行时调用转义函数(如 addslashes())更有效率。 尽管 php.ini-dist 默认打开了这个选项,但是 php.ini-recommended 默认却关闭了它,主要是出于性能的考虑。

(3)不便

由于不是所有数据都需要转义,在不需要转义的地方看到转义的数据就很烦。比如说通过表单发送邮件,结果看到一大堆的 \’。针对这个问题,可以使用 stripslashes() 函数处理。

0X08 mysqli_real_escape_string/mysqli_escape_string –> (PHP >= 5 ,PHP 7)

此函数用来对字符串中的特殊字符进行转义, 以使得这个字符串是一个合法的 SQL 语句。传入的字符串会根据当前连接的字符集进行转义,得到一个编码后的合法的 SQL 语句。mysqli_escape_string 是 mysqli_real_escape_string 的别名。

用法:

mysqli_real_escape_string(connection,escapestring);

参数解释:

connection 必需。规定要使用的 MySQL 连接。
escapestring 必需。要转义的字符串。编码的字符是 NUL(ASCII 0)、\n、\r、\、’、” 和 Control-Z。

返回值:

返回已转义的字符串。

注意:

1.调用 mysqli_real_escape_string() 函数之前, 必须先通过调用 mysqli_set_charset() 函数或者在 MySQL 服务器端设置字符集
2.mysqli_character_set_name() 返回当前数据库连接的默认字符编码

0X09 prepare 预编译

通过使用预编译语句(prepared statements)和参数化查询(parameterized queries)。这些sql语句从参数,分开的发送到数据库服务端,进行解析。这样黑客不可能插入恶意sql代码。

对应的就是下面这两种方法:

1.使用PDO对象(对于任何数据库驱动都好用)

$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); 
$stmt->execute(array('name' => $name)); 
foreach ($stmt as $row) {
// do something with $row 
}

2. 使用MySqli

$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?'); 
$stmt->bind_param('s', $name); 
$stmt->execute(); 
$result = $stmt->get_result(); 
while ($row = $result->fetch_assoc()) {
// do something with $row 
}

正确地建立连接:

注意:当使用PDO去连接Mysql数据库时,真正的预处理默认并没有开启。为了开启他,你应该关闭模拟的预处理语句,以下是一个例子:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 

$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

在上面的例子里,错误模式严格意义上来说没有必要,但推荐你加上去。这样,脚本在遇到致命错误(Fatal Error)的时候并不会停止运行。并且给开发者去捕获(catch )那些PDOException异常。

第一个setAttribute()是必须的。这告诉PDO去关闭模拟预处理,然后使用真正的预处理语句。这将保证语句和值在被交到Mysql服务器上没有被解析(让攻击者没有机会去进行sql注入。)

尽管你可以在构造函数里设置字符集(charset ),但你也要注意旧版本的PHP(<5.3.6)会忽略在DSN中设置的字符集参数。

解释

到底发生了什么呢?你的SQL语句交给prepare 之后被数据库服务器解析和编译了。通过制定参数(不管是“?”还是命名占位符:name),你都可以告诉数据库引擎哪里你想过滤掉。然后当你执行execute方法时,预处理语句会把你所指定的参数值结合起来。

这里很重要的就是参数值和编译过的语句绑定在了一起,而不是简简单单的SQL字符串、SQL注入通过骗起脚本加入一些恶意的字符串,在建立sql发送到数据库的时候产生后果。所以,通过分离的从参数中发送真正的sql语句,你控制了风险:在结尾的时候你不打算干的一些事。(译者注:请看开篇的例子)。当你使用预编译的时候,任何参数都会被当作字符串。在这个例子里,如果$name变量包含了’Sarah’; DELETE FROM employees 这个结果只会简单的搜索字符串“‘Sarah’; DELETE FROM employees”,所以你不会得到一张空表。

另外一个使用预编译的好处就是,如果你在同一个会话中执行一个statement多次,只会被解析和编译一次,对速度更友好。

哦,既然你问了增加语句的时候怎么使用,下面给你个例子:

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');   
$preparedStatement->execute(array('column' => $unsafeValue));

PDO如何解决sql注入

完整代码:

<?php
$pdo = new PDO("mysql:host=192.168.0.1;dbname=test;charset=utf8","root");

$st = $pdo->prepare("select * from info where id =? and name = ?");

$id = 21;

$name = 'zhangsan';

$st->bindParam(1,$id);
$st->bindParam(2,$name);
$st->execute();

$st->fetchAll();
?>

在php5.3.6之后,pdo不会在本地对sql进行拼接然后将拼接后的sql传递给mysql server处理(也就是不会在本地做转义处理)。pdo的处理方法是在prepare函数调用时,将预处理好的sql模板(包含占位符)通过mysql协议传递给mysql server,告诉mysql server模板的结构以及语义。当调用execute时,将两个参数传递给mysql server。由mysql server完成变量的转移处理。将sql模板和变量分两次传递,即解决了sql注入问题。

0X10 补充:使用了PDO就一定安全了吗???

建议去看一下PDO 的官方文档,文章中有这样一句话:

the developer can be sure that no SQL injection will occur (however,
if other portions of the query are being built up with unescaped
input, SQL injection is still possible).

翻译过来就是

开发人员可以确保不会发生SQL注入(然而,如果查询的其他部分是用未转义输入构建的,那么SQL注入就仍然可能)。

因为有些查询语句并不适合使用PDO 进行处理,可能使用PDO处理比较困困难,于是就有一些没有做处理,还有就是有些挂羊头卖狗肉(估计开发的也不懂PDO),真正用的时候还是老方法,再有就是开发人员对PDO本地预处理的错误开放,以及一些编码问题的处理上可能还是存在问题。

当然这是面试经常问的问题,请看这三篇文章,虽然有点老,但是我认为对原理的理解还是很有帮助的。

http://zhangxugg-163-com.iteye.com/blog/1835721
http://zhangxugg-163-com.iteye.com/blog/1855088
http://zhangxugg-163-com.iteye.com/blog/1850461